package drds.datasource.connection_pool.impl;


import com.sun.istack.internal.Nullable;
import drds.datasource.connection_pool.api.SqlException;
import drds.datasource.connection_pool.impl.holder.ConnectionHolder;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import tool.plus.Constants;

import java.sql.*;
import java.util.concurrent.locks.ReentrantLock;

//import drds.common.Constants;

@Slf4j
public class PooledConnection implements drds.datasource.connection_pool.api.Connection {

    @Setter
    @Getter
    protected final Thread ownerThread;
    @Setter
    @Getter
    final ReentrantLock lock;
    @Setter
    @Getter
    private final boolean dupCloseLogEnable;
    @Setter
    @Getter
    protected Connection connection;
    @Setter
    @Getter
    protected volatile ConnectionHolder connectionHolder;
    @Setter
    @Getter
    protected volatile boolean traceEnable = false;
    @Setter
    @Getter
    protected volatile boolean closed = false;
    @Setter
    @Getter
    protected StackTraceElement[] stackTraceElements;
    @Setter
    @Getter
    protected Throwable disableError = null;
    @Setter
    @Getter
    private volatile boolean disable = false;
    @Setter
    @Getter
    private long connectedTimeMillis;
    @Setter
    @Getter
    private long connectedTimeNano;
    @Setter
    @Getter
    private volatile boolean running = false;
    @Setter
    @Getter
    private volatile boolean abandoned = false;

    public PooledConnection(ConnectionHolder connectionHolder) {

        this.connection = connectionHolder.getConnection();
        this.connectionHolder = connectionHolder;
        this.lock = connectionHolder.lock;
        dupCloseLogEnable = connectionHolder.getDataSource().isDupCloseLogEnable();
        ownerThread = Thread.currentThread();
        connectedTimeMillis = System.currentTimeMillis();
    }

    /**
     * log
     */
    public static void log(@Nullable Logger log, SQLException e, String method) {
        if (Constants.developMode) {
            e.printStackTrace();
        }
        log.error(method, e);
    }

    public void setConnectedTimeNano() {
        if (connectedTimeNano <= 0) {
            this.setConnectedTimeNano(System.nanoTime());
        }
    }


    public SqlException handleException(SQLException e, String method, String sql) throws SqlException {
        if (Constants.developMode) {
            e.printStackTrace();
        }
        log.error(method, e);
        //
        final ConnectionHolder connectionHolder = this.connectionHolder;
        if (connectionHolder != null) {
            AbstractDataSource dataSource = connectionHolder.getDataSource();
            dataSource.handleConnectionException(this, e, sql);
        }
        throw new SqlException(e);
    }


    public void closePoolableStatement(PooledPreparedStatement pooledPreparedStatement) throws SqlException {
        if (connectionHolder == null) {
            return;
        }
        try {
            // Connection behind the preparedStatement may be in invalid state, which will throw a
            // SQLException.
            // In this case, the sql_exception is desired to be properly handled to remove the
            // unusable connection from the impl.
            pooledPreparedStatement.super$close();
        } catch (SqlException ex) {
            this.handleException(ex, " pooledPreparedStatement.super$close()", null);
            throw ex;
        } finally {
            connectionHolder.getDataSource().incrementClosedPreparedStatementCount();
        }
    }


    public Connection getConnection() {
        return connection;
    }


    @Override
    public void close() throws SqlException {
        if (this.disable) {
            return;
        }

        ConnectionHolder connectionHolder = this.connectionHolder;
        if (connectionHolder == null) {
            if (dupCloseLogEnable) {
                log.error("dup close");
            }
            return;
        }

        AbstractDataSource dataSource = connectionHolder.getDataSource();
        boolean isSameThread = this.getOwnerThread() == Thread.currentThread();

        if (!isSameThread) {
            dataSource.setAsyncCloseConnectionEnable(true);
        }

        if (dataSource.isAsyncCloseConnectionEnable()) {
            syncClose();
            return;
        }


        recycle();

        this.disable = true;
    }

    public void syncClose() throws SqlException {
        lock.lock();
        try {
            if (this.disable) {
                return;
            }

            ConnectionHolder connectionHolder = this.connectionHolder;
            if (connectionHolder == null) {
                if (dupCloseLogEnable) {
                    log.error("dup close");
                }
                return;
            }


            recycle();

            this.disable = true;
        } finally {
            lock.unlock();
        }
    }

    @Override
    public boolean isClosed() throws SqlException {
        if (connectionHolder == null) {
            return true;
        }

        return closed || disable;
    }

    //
    public void disable() {
        disable(null);
    }

    public void disable(SQLException e) {
        this.traceEnable = false;
        this.connectionHolder = null;
        this.disable = true;
        this.disableError = e;
    }

    //关于回收
    public void abandond() {
        this.abandoned = true;
    }

    public void recycle() throws SqlException {
        if (this.disable) {
            return;
        }

        ConnectionHolder connectionHolder = this.connectionHolder;
        if (connectionHolder == null) {
            if (dupCloseLogEnable) {
                log.error("dup close");
            }
            return;
        }

        if (!this.abandoned) {
            AbstractDataSource dataSource = connectionHolder.getDataSource();
            dataSource.recycle(this);
        }

        this.connectionHolder = null;
        connection = null;
        //transactionInfo = null;
        closed = true;
    }

    //
    public void checkState() throws SqlException {
        final boolean asyncCloseEnabled;
        if (connectionHolder != null) {
            asyncCloseEnabled = connectionHolder.getDataSource().isAsyncCloseConnectionEnable();
        } else {
            asyncCloseEnabled = false;
        }

        if (asyncCloseEnabled) {
            lock.lock();
            try {
                checkStateInternal();
            } finally {
                lock.unlock();
            }
        } else {
            checkStateInternal();
        }
    }

    private void checkStateInternal() throws SqlException {
        if (closed) {
            if (disableError != null) {
                throw new SqlException("connection closed", disableError);
            } else {
                throw new SqlException("connection closed");
            }
        }

        if (disable) {
            if (disableError != null) {
                throw new SqlException("connection disabled", disableError);
            } else {
                throw new SqlException("connection disabled");
            }
        }

        if (connectionHolder == null) {
            if (disableError != null) {
                throw new SqlException("connection connectionHolder is null", disableError);
            } else {
                throw new SqlException("connection connectionHolder is null");
            }
        }
    }

    public Throwable getDisableError() {
        return disableError;
    }
    //


    //
    //下面是api

    @Override
    public String getCatalog() throws SqlException {
        checkState();
        try {
            return connection.getCatalog();
        } catch (SQLException e) {
            throw handleException(e, "connection.getCatalog()", null);
        }
    }

    @Override
    public void setCatalog(String catalog) throws SqlException {
        checkState();
        try {
            connection.setCatalog(catalog);
        } catch (SQLException ex) {
            handleException(ex, "connection.setCatalog(catalog)", null);
        }
    }

    @Override
    public DatabaseMetaData getDatabaseMetaData() throws SqlException {
        try {
            return connection.getMetaData();
        } catch (SQLException e) {
            throw handleException(e, "connection.getMetaData()", null);
        }
    }

    @Override
    public boolean getAutoCommit() throws SqlException {
        checkState();

        try {
            return connection.getAutoCommit();
        } catch (SQLException e) {
            throw handleException(e, "connection.getAutoCommit()", null);
        }
    }

    //
    @Override
    public void setAutoCommit(boolean autoCommit) throws SqlException {
        checkState();

        boolean useLocalSessionState = connectionHolder.getDataSource().isUseLocalSessionState();

        if (useLocalSessionState) {
            if (autoCommit == connectionHolder.underlyingAutoCommit) {
                return;
            }
        }

        try {
            connection.setAutoCommit(autoCommit);
            connectionHolder.setUnderlyingAutoCommit(autoCommit);
        } catch (SQLException e) {

            handleException(e, "connection.setAutoCommit(autoCommit)", null);
        }
    }

    @Override
    public void commit() throws SqlException {
        checkState();

        AbstractDataSource dataSource = connectionHolder.getDataSource();
        dataSource.incrementCommitCount();

        try {
            connection.commit();
        } catch (SQLException e) {
            handleException(e, "connection.commit()", null);
        } finally {

        }
    }

    @Override
    public void rollback() throws SqlException {

        if (connectionHolder == null) {
            return;
        }

        AbstractDataSource dataSource = connectionHolder.getDataSource();
        dataSource.incrementRollbackCount();

        try {
            connection.rollback();
        } catch (SQLException e) {
            handleException(e, "connection.rollback()", null);
        } finally {
        }
    }

    //
    @Override
    public Savepoint setSavepoint() throws SqlException {
        checkState();

        try {
            return connection.setSavepoint();
        } catch (SQLException ex) {
            handleException(ex, "connection.setSavepoint()", null);
            return null;
        }
    }

    @Override
    public Savepoint setSavepoint(String name) throws SqlException {
        checkState();

        try {
            return connection.setSavepoint(name);
        } catch (SQLException ex) {
            handleException(ex, "connection.setSavepoint(name)", null);
            return null; // never arrive
        }
    }

    @Override
    public void rollback(Savepoint savepoint) throws SqlException {
        if (connectionHolder == null) {
            return;
        }

        AbstractDataSource dataSource = connectionHolder.getDataSource();
        dataSource.incrementRollbackCount();

        try {
            connection.rollback(savepoint);
        } catch (SQLException ex) {
            handleException(ex, "connection.rollback(savepoint);", null);
        } finally {

        }
    }


    @Override
    public void releaseSavepoint(Savepoint savepoint) throws SqlException {
        checkState();
        try {
            connection.releaseSavepoint(savepoint);
        } catch (SQLException ex) {
            handleException(ex, "connection.releaseSavepoint(savepoint)", null);
        }
    }


    //
    @Override
    public drds.datasource.connection_pool.api.Statement createStatement() throws SqlException {
        checkState();
        Statement statement = null;
        try {
            statement = connection.createStatement();
        } catch (SQLException ex) {
            handleException(ex, "connection.createStatement()", null);
        }
        connectionHolder.getDataSource().initStatement(this, statement);
        PooledStatement pooledStatement = new PooledStatement(this, statement);
        connectionHolder.addPooledStatement(pooledStatement);
        return pooledStatement;
    }

    @Override
    public drds.datasource.connection_pool.api.PreparedStatement prepareStatement(String sql) throws SqlException {
        checkState();

        PreparedStatement preparedStatement = null;
        try {
            preparedStatement = connection.prepareStatement(sql);
        } catch (SQLException e) {
            handleException(e, "connection.createStatement()", sql);
        }


        initStatement(preparedStatement);

        PooledPreparedStatement pooledPreparedStatement = new PooledPreparedStatement(this, preparedStatement);

        connectionHolder.addPooledStatement(pooledPreparedStatement);

        return pooledPreparedStatement;
    }

    private void initStatement(PreparedStatement preparedStatement) throws SqlException {

        connectionHolder.getDataSource().initStatement(this, preparedStatement);
    }


    @Override
    public drds.datasource.connection_pool.api.PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SqlException {
        checkState();


        PreparedStatement preparedStatement = null;
        try {
            preparedStatement = connection.prepareStatement(sql, autoGeneratedKeys);
        } catch (SQLException e) {
            handleException(e, "connection.prepareStatement(sql, autoGeneratedKeys)", sql);
        }
        initStatement(preparedStatement);

        PooledPreparedStatement pooledPreparedStatement = new PooledPreparedStatement(this, preparedStatement);

        connectionHolder.addPooledStatement(pooledPreparedStatement);

        return pooledPreparedStatement;
    }

    //
    //

    boolean isRunning() {
        return running;
    }

    final void beforeExecute() {
        final ConnectionHolder connectionHolder = this.connectionHolder;
        if (connectionHolder != null && connectionHolder.dataSource.supportRemoveAbandoned) {
            running = true;
        }
    }

    final void afterExecute() {
        final ConnectionHolder connectionHolder = this.connectionHolder;
        if (connectionHolder != null) {
            AbstractDataSource dataSource = connectionHolder.dataSource;
            if (dataSource.supportRemoveAbandoned) {
                running = false;
                connectionHolder.lastActiveTimeMillis = System.currentTimeMillis();
            }
            dataSource.onFatalError = false;
        }
    }


}
